/*
 *
 * Copyright (C) 2012 Walter Lütgenau
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
package offset.nodes.client.veditor.model;

import offset.nodes.client.editor.model.messages.GetVirtualInformation;
import offset.nodes.client.editor.model.messages.UploadData;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.StringTokenizer;

import javax.jcr.Node;
import javax.jcr.PropertyType;
import javax.jcr.Value;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.Element;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.html.HTML;

import offset.nodes.Constants;
import offset.nodes.client.chooser.model.GetNodeByPath;
import offset.nodes.client.virtual.model.SimpleQuery;
import offset.nodes.client.model.ServerModel;
import offset.nodes.client.virtual.model.jcr.NodeReader;
import offset.nodes.client.editor.Editor;
import offset.nodes.client.editor.model.ComponentModel;
import offset.nodes.client.editor.model.DocumentContext;
import offset.nodes.client.editor.model.EditorModel;
import offset.nodes.client.editor.model.StructureTree;
import offset.nodes.client.model.CommentedUserRequest;
import offset.nodes.client.model.NodeTypes;
import offset.nodes.client.virtual.model.jcr.nodetype.NodeTypeDefinition;
import offset.nodes.client.virtual.model.jcr.nodetype.PropertyDefinition;

public class DataModel extends EditorModel {

    public static final String ATTRIBUTE_NAME_DATA = Constants.ATTRIBUTE_DATA;
    public static final String ATTRIBUTE_NAME_TEMPLATE = "n-template";
    public static final String ATTRIBUTE_NAME_SCHEMA = "n-schema";
    public static final String ATTRIBUTE_NAME_PATH = "n-path";
    public static final String ATTRIBUTE_NAME_ID = "n-id";
    public static final String ATTRIBUTE_NAME_VIEW = "n-view";
    public static final String ATTRIBUTE_NAME_EDIT = "n-edit";
    public static final String ATTRIBUTE_NAME_PRIMARY_REFERENCE = "n-primary-reference";
    public static final String ATTRIBUTE_NAME_BINARY = "n-binary";
    public static final String ATTRIBUTE_NAME_EXTERNAL_TEMPLATE = "n-external-template";
    public static final String ATTRIBUTE_NAME_SELECT = "n-select";
    public static final String ATTRIBUTE_NAME_ACTION = "n-action";
    public static final String ATTRIBUTE_NAME_PAGE = "n-page";
    public static final String ATTRIBUTE_VALUE_EXPLICIT = "explicit";
    public static final String ATTRIBUTE_VALUE_TRUE = "true";
    public static final String ATTRIBUTE_VALUE_FALSE = "false";
    public static final String ATTRIBUTE_VALUE_COMPONENT_ID = "{@jcr:uuid}";
    public static final String SECONDARY_QUERY = "/secondary";
    public static final String ELEMENT_DOCUMENT = "document";
    public static final String XSLT_NAME = "name()";
    public static final HTML.Tag RUN_TAG = HTML.Tag.DFN;
    public static final String VIEW_SELECT_ENUMERATED_VALUE = "offset.nodes.editor.controller.EnumeratedValueChooser";
    HashMap properties = null;

    public HashMap getProperties() {
        return properties;
    }
    
    /**
     * Return the node type information repository.
     * 
     * @return the node type information repository or null.
     */
    public NodeTypes getNodeTypes() {
        if (getProperties() == null)
            return null;
        
        return (NodeTypes) getProperties().get(Editor.PROP_NODE_TYPES);
    }

    public GetVirtualInformation.Response readVirtualInformation(String transformationPath, String nodeTypeName) throws Exception {
        String serverUrl = (String) properties.get(Editor.PROP_UPLOAD_TO);
        String instancePath = (String) properties.get(Editor.PROP_INSTANCE_PATH);
        ServerModel serverModel = new ServerModel(new URL(serverUrl));

        GetVirtualInformation.Request request = new GetVirtualInformation.Request();
        request.setNodeTypeName(nodeTypeName);
        request.setTransformationPath(transformationPath);
        request.setInstancePath(instancePath);

        GetVirtualInformation.Response response = (GetVirtualInformation.Response) serverModel.sendRequest(request);

        return response;
    }

    protected Node getNode(String path) throws Exception {
        String serverUrl = (String) properties.get(Editor.PROP_UPLOAD_TO);
        ServerModel serverModel = new ServerModel(new URL(serverUrl));

        GetNodeByPath.Request request = new GetNodeByPath.Request();
        request.setPath(path);

        GetNodeByPath.Response response = (GetNodeByPath.Response) serverModel.sendRequest(request);
        NodeReader reader = new NodeReader();
        return reader.read(new ByteArrayInputStream(response.getNode().getBytes()));
    }
    
    SecondaryQueries secondaryQueries = new SecondaryQueries();

    class SecondaryQueries {

        HashMap<String, SimpleQuery> secondaryQueries = null;

        public SimpleQuery getQuery(String secondaryQueryName) throws Exception {
            if (secondaryQueries == null)
                secondaryQueries = init();
            return secondaryQueries.get(secondaryQueryName);
        }

        protected HashMap<String, SimpleQuery> init() throws Exception {
            String instancePath = (String) properties.get(Editor.PROP_INSTANCE_PATH);
            if (instancePath == null) // not in data editor mode
                return null;

            Node instance = getNode(instancePath);
            if (!instance.hasProperty(Constants.PROP_SECONDARY_QUERY))
                return null;

            Value[] secondaryQueries = instance.getProperty(Constants.PROP_SECONDARY_QUERY).getValues();
            HashMap<String, SimpleQuery> result = new HashMap<String, SimpleQuery>();
            for (int i = 0; i < secondaryQueries.length; i++) {
                String queryString = secondaryQueries[i].getString();
                SecondaryQuery secondaryQuery = new SecondaryQuery(queryString);
                SimpleQuery query = new SimpleQuery(secondaryQuery.getQuery());
                result.put(secondaryQuery.getName(), query);
            }
            return result;
        }
    }

    public SimpleQuery getSecondaryQuery(String name) throws Exception {
        return secondaryQueries.getQuery(name);
    }

    public DataModel(DocumentContext context, HashMap properties) {
        super(context);
        this.properties = properties;
    }

    public String getPath(Element element) {
        return (String) element.getAttributes().getAttribute(DataModel.ATTRIBUTE_NAME_PATH);
    }

    public String getSchemaName(Element element) {
        return (String) element.getAttributes().getAttribute(DataModel.ATTRIBUTE_NAME_SCHEMA);
    }

    public String getTemplateName(Element element) {
        return (String) element.getAttributes().getAttribute(DataModel.ATTRIBUTE_NAME_TEMPLATE);
    }

    public boolean isTemplateElement(Element element) {
        return element.getAttributes().isDefined(DataModel.ATTRIBUTE_NAME_TEMPLATE);
    }

    public boolean isSchemaElement(Element element) {
        return element.getAttributes().isDefined(DataModel.ATTRIBUTE_NAME_SCHEMA);
    }

    public boolean isTemplate(Element e) {
        if (!e.getName().equals(HTML.Tag.DIV.toString()))
            return false;
        /*if ( removable.getAttributes().getAttribute(ATTRIBUTE_NAME_TEMPLATE) == null)
        return false;*/

        return true;
    }

    public boolean isInTemplate(Element e) {
        if (e.getAttributes().getAttribute(ATTRIBUTE_NAME_TEMPLATE) == null)
            return false;

        return true;
    }

    public boolean isInTemplate(int pos) {
        return isInTemplate(getDocument().getParagraphElement(pos));
    }

    public InputStream getInputStream(String path) throws MalformedURLException, IOException {
        URL base = getDocument().getBase();
        if (!base.getProtocol().startsWith("http"))
            return new FileInputStream(path);

        // only first path components (= context + servlet), there will be a specific action for the data
        StringTokenizer parser = new StringTokenizer(base.getPath(), "/");
        int i = 0;
        StringBuffer buf = new StringBuffer();
        while (parser.hasMoreTokens()) {
            String token = parser.nextToken();
            buf.append("/" + token);
            i++;
            if (i >= 2)
                break;
        }

        URL url = new URL(base.getProtocol(), base.getHost(), base.getPort(), buf.toString() + path + "?mode=binary");
        InputStream in = url.openConnection().getInputStream();

        return in;
    }

    public Element showView(int pos) throws Exception {
        Element e = getDocument().getCharacterElement(pos);
        VirtualBlock vb = VirtualBlock.getVirtualBlock(e, this);
        return vb.showView();
    }

    public Element moveCaret(int oldPos, int newPos) throws Exception {
        Element e = getDocument().getCharacterElement(newPos);
        VirtualBlock vb = VirtualBlock.getVirtualBlock(e, this);
        return vb.moveCaret(oldPos, newPos);
    }

    public VirtualBlock getVirtualBlock(Element e) throws Exception {
        VirtualBlock virtualBlock = VirtualBlock.getVirtualBlock(e, this);
        if (virtualBlock == null)
            return null;

        return virtualBlock;

    }

    /**
     * Save the state of component editors, if there are any
     * 
     * @param e
     * @throws Exception
     */
    public void saveComponents(Element e, CommentedUserRequest comment) throws Exception {
        VirtualBlock vb = getVirtualBlock(e);
        if (vb != null)
            vb.saveComponents(comment);
    }

    public Template getTemplate(Element e) throws Exception {
        VirtualBlock virtualBlock = VirtualBlock.getVirtualBlock(e, this);
        if (virtualBlock == null)
            return null;

        return virtualBlock.getTemplateByElementPath(getDataPath(e));
    }

    public Element getRoot() {
        return getDocument().getRootElements()[0];
    }

    public Element getTemplateElement(String templateName) {
        Element root = getDocument().getRootElements()[0];
        return getTemplateElement(root, templateName);
    }

    protected Element getTemplateElement(Element e, String templateName) {
        if (e.getAttributes().isDefined(ATTRIBUTE_NAME_TEMPLATE) && (((String) e.getAttributes().getAttribute(ATTRIBUTE_NAME_TEMPLATE)).equals(templateName)))
            return e;

        for (int i = 0; i < e.getElementCount(); i++) {
            Element found = getTemplateElement(e.getElement(i), templateName);
            if (found != null)
                return found;
        }

        return null;
    }

    public Template getTemplate(VirtualElement e) {
        return e.getVirtualBlock().getTemplateByName(e.getName());
    }

    public Template getTemplate(String styleSheetPath, String name) throws Exception {
        Element vb = getTemplateElement(styleSheetPath);
        if (vb == null)
            return null;

        VirtualBlock virtualBlock = VirtualBlock.getVirtualBlock(vb, this);
        if (virtualBlock == null)
            return null;

        return virtualBlock.getTemplateByElementPath(name);
    }

    public void addTemplate(Element styleSheetElement, File styleSheet) {
        SimpleAttributeSet atts = new SimpleAttributeSet(styleSheetElement.getAttributes());
        atts.addAttribute(ATTRIBUTE_NAME_TEMPLATE, styleSheet.getPath());
        getDocument().setElementAttributes(styleSheetElement, atts, true);
    }

    public Element findTemplate(Element e) {
        if (e.getName().equals(HTML.Tag.DIV.toString()) && e.getAttributes().isDefined(ATTRIBUTE_NAME_DATA))
            return e;
        for (int i = 0; i < e.getElementCount(); i++) {
            Element result = findTemplate(e.getElement(i));
            if (result != null)
                return result;
        }

        return null;

    }

    public Element findTemplate() {
        return findTemplate(getDocument().getRootElements()[0]);
    }

    public String saveTemplate(boolean editor) throws BadLocationException {
        Element template = findTemplate(getDocument().getRootElements()[0]);

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        PrintStream pout = new PrintStream(out);
        StyleSheetWriter writer = new StyleSheetWriter(template, editor, this);
        writer.write(pout);

        return new String(out.toByteArray());
    }

    public String saveEditorTemplate() throws BadLocationException {
        return saveTemplate(true);
    }

    public String saveTemplate() throws BadLocationException {
        return saveTemplate(false);
    }

    public void saveTemplate(Element styleSheetElement, OutputStream out) throws BadLocationException {
        PrintStream pout = new PrintStream(out);
        StyleSheetWriter writer = new StyleSheetWriter(styleSheetElement, this);
        writer.write(pout);
    }

    public String getDataPath(Element e) {
        return discardFilters(getData(e));
    }

    public String getData(Element e) {
        String data = null;

        if (isArtificialWithInlineElement(e))
            data = (String) getFirstInlineAttributes(e).getAttribute(ATTRIBUTE_NAME_DATA);
        else
            data = (String) e.getAttributes().getAttribute(ATTRIBUTE_NAME_DATA);

        return data;
    }

    public String getId(Element e) {
        String id = (String) e.getAttributes().getAttribute(ATTRIBUTE_NAME_ID);
        return id;
    }

    public Element getDataElement(int pos) {
        Element e = getHtmlElement(getDocument().getParagraphElement(pos));
        for (int i = 0; i < e.getElementCount(); i++)
            if (e.getElement(i).getAttributes().isDefined(ATTRIBUTE_NAME_DATA))
                return e.getElement(i);

        while (e != null && !isData(e)) {
            e = e.getParentElement();
        }

        return e;
    }

    /**
     * Return the selectable virtual section of the virtual page.
     *
     * This is either a binary section or a paragraph section.
     * 
     * @param pos
     * @return
     */
    public Element getVirtualSection(int pos) {
        Element binary = getBinary(pos);
        if (binary != null)
            return binary;

        return getRealParagraph(pos);
    }
    
        
    /**
     * Return the property type of a property characterized by a 
     * node type path. 
     * 
     * The latter is an XPATH path, the components of which are node type prefix names.
     * Only the last component may be the prefix name of a property, it will have a "@"
     * as first character, then.
     * 
     * @param nodeTypePath the node type path
     * @return the PropertyType or -1, if not found.
     */
    public int getPropertyType(String nodeTypePath) {
        NodeTypes nodeTypes = getNodeTypes();
        if (nodeTypes == null)
            return -1;
        
        String[] typePathElements = nodeTypePath.split(Constants.PATH_SEPARATOR);
        if (typePathElements.length < 2
                || typePathElements[typePathElements.length - 1].indexOf(Constants.QUERY_ATTRIBUTE_KEY) != 0)
            return -1;
        
        NodeTypeDefinition nodeType = nodeTypes.getNodeType(typePathElements[typePathElements.length - 2]);
        if (nodeType == null)
            return -1;
        
        PropertyDefinition property = nodeTypes.getPropertyDefinition(nodeType, typePathElements[typePathElements.length - 1].substring(1));
        if (property == null)
            return -1;
        
        return property.getRequiredType();
    }
    
    /**
     * Is the position within a data element associated to a reference property?
     * 
     * @param pos the position
     * @return is within such a data element?
     */
    public boolean inReferenceElement(int pos) {
        Element e = getDocument().getParagraphElement(pos);
        while (e != null) {
            if (hasData(e)) {
                if (getPropertyType(getData(e)) == PropertyType.REFERENCE)
                        return true;
                return false;
            }
            
            e = e.getParentElement();
        }
        
        return false;
    }
    
    /**
     * Is the position within an element, which shall be treated as transparent?
     * 
     * The substructure of a transparent element will be ignored in the virtual editor.
     * Transparent elements will be e.g. hyperlinks associated to reference properties or binary elements, i.e. 
     * elements associated to another virtual page, that shall be treated as a single element.
     * 
     * @param pos the position
     * @return the decision.
     */
    public boolean inTransparentElement(int pos) {
        return inReferenceElement(pos)
                || inBinary(pos);
    }

    /**
     * The position is within a binary element, if the ATTRIBUTE_NAME_BINARY is
     * defined in the parent hierarchy of the paragraph element
     * associated to the position.
     * 
     * @param pos
     * @return
     */
    public boolean inBinary(int pos) {
        return getBinary(pos) != null;
    }

    /**
     * Return the enclosing binary element, if there is any.
     * 
     * @param pos
     * @return
     */
    public Element getBinary(int pos) {
        Element e = getDocument().getParagraphElement(pos);

        while (e != null) {
            if (e.getAttributes().isDefined(ATTRIBUTE_NAME_BINARY))
                return e;

            e = e.getParentElement();
        }

        return null;
    }

    /**
     * Is this element a data element, i.e. has it a data attribute?
     * @param pos
     * @return
     */
    public boolean isData(int pos) {
        return isData(getDocument().getParagraphElement(pos));
    }

    /**
     * Is this element a data element, i.e. has it a data attribute?
     * @param e the element
     * @return the answer
     */
    public boolean isData(Element e) {
        if (e.getAttributes().isDefined(ATTRIBUTE_NAME_DATA))
            return true;

        return false;
    }

    /**
     * Is this element a data element, i.e. has it a data attribute?
     * @param e the element
     * @return the answer
     */
    public boolean isData(AttributeSet atts) {
        if (atts.isDefined(ATTRIBUTE_NAME_DATA))
            return true;

        return false;
    }

    /**
     * Has this element associated data? This is true if 
     * - the element has a data attribute itself
     * - the element is an inline element with a data attribute in the inline attributes
     * @param e
     * @return the answer
     */
    public boolean hasData(Element e) {
        if (hasInlineDataElement(e))
            return true;

        if (isData(e))
            return true;

        return false;
    }

    /**
     * Does the element have associated data?
     *
     * Only HTML elements can have associated data, as the HTML is used
     * 1. to associate data in the template editor
     * 2. to persist the data association in the sample HTML.
     * 
     * @param pos
     * @return
     */
    public boolean hasData(int pos) {
        Element e = getHtmlElement(getDocument().getParagraphElement(pos));

        // The paragraph element itself is associated to the data
        if (e.getAttributes().isDefined(ATTRIBUTE_NAME_DATA))
            return true;

        // a subelement (i.e. a hyperlink) is associated to data
        for (int i = 0; i < e.getElementCount(); i++)
            if (e.getElement(i).getAttributes().isDefined(ATTRIBUTE_NAME_DATA))
                return true;

        return false;
    }

     public boolean onlyDescriptionText(Element e) throws Exception {
        String templateName = getTemplateName(e);
        Template template = getTemplate(templateName, discardFilters(getData(e)));

        int length = e.getEndOffset() - e.getStartOffset();
        String text = getDocument().getText(e.getStartOffset(), length - 1);

        String description = template.getDescriptionText();
        if (text.equals(description))
            return true;

        if ((isArtificialWithInlineElement(e) || e instanceof EditorModel.InlineHtmlElement) && text.length() == description.length() + 1 && text.substring(0, text.length() - 1).equals(description)) 
            return true;

        return false;
    }

    public boolean isContent(Element e) {
        String name = e.getName();
        if (name.equals(HTML.Tag.IMPLIED.toString()) || name.equals(HTML.Tag.CONTENT.toString()))
            if (!isArtificialWithInlineElement(e))
                return true;
        return false;
    }

    /**
     * Check, if binary attribute is set
     *
     * @param e
     * @return
     */
    public boolean isBinary(Element e) {
        if (isArtificialWithInlineElement(e))
            e = getHtmlElement(e).getElement(0);
        return e.getAttributes().isDefined(ATTRIBUTE_NAME_BINARY);
    }

    /**
     * Check, if binary attribute is set
     *
     * @param e
     * @return
     */
    public boolean isComponent(Element e) {
        return e.getAttributes().isDefined(ComponentModel.ATT_COMPONENT_ID);
    }

    /**
     * If this is a binary element, check the size.
     *
     * Empty binary elements will consist of a div with only a p-implied of size 1 (holding the break).
     *
     * @param e
     * @return
     */
    public boolean isEmptyBinary(Element e) {
        if (!isBinary(e))
            return false;

        return e.getEndOffset() - e.getStartOffset() <= 1;
    }

    /**
     * Is this an inline element associated to data?
     * 
     * @param e The element
     * @return The answer: true or false
     */
    public boolean isInlineDataElement(Element e) {
        AttributeSet atts = getInlineAttributes(e);
        if (atts == null)
            return false;
        return isData(atts);
    }

    /**
     * Return the attribute set of the first inline element, e.g. A or SPAN. The use of this routing assumes, that there will be only a single
     * inline element.
     * 
     * @param e The element with inline child elements
     * @return the data associated to the first inline element
     */
    public AttributeSet getFirstInlineAttributes(Element e) {
        String name = e.getName();
        if (name.equals(HTML.Tag.IMPLIED.toString()) || name.equals(HTML.Tag.P.toString()))
            for (int i = 0; i < e.getElementCount(); i++) {
                HTML.Tag tag = getInlineElementTag(e.getElement(i));
                if (tag != null)
                    return (AttributeSet) e.getElement(i).getAttributes().getAttribute(tag);
            }

        return null;

    }

    /**
     * Return the HTML tag of the first inline element, e.g. A or SPAN. The use of this routing assumes, that there will be only a single
     * inline element.
     * 
     * @param e The element with inline child elements
     * @return the tag associated to the first inline element
     */
    public HTML.Tag getFirstInlineTag(Element e) {
        String name = e.getName();
        if (name.equals(HTML.Tag.IMPLIED.toString()))
            for (int i = 0; i < e.getElementCount(); i++) {
                HTML.Tag tag = getInlineElementTag(e.getElement(i));
                if (tag != null)
                    return tag;
            }

        return null;

    }

    public boolean hasInlineDataElement(Element e) {
        if (!isArtificialWithInlineElement(e))
            return false;

        AttributeSet atts = getFirstInlineAttributes(e);
        if (atts.isDefined(ATTRIBUTE_NAME_DATA))
            return true;

        return false;
    }

    class InlineElement implements Element {

        Element leaf;
        HTML.Tag tag;
        SimpleAttributeSet attributes;

        public InlineElement(Element leaf, HTML.Tag tag, AttributeSet atts) {
            this.leaf = leaf;
            this.tag = tag;
            this.attributes = new SimpleAttributeSet(atts);
            this.attributes.addAttribute(StyleConstants.NameAttribute, tag.toString());
        }

        public boolean isLeaf() {
            return leaf.isLeaf();
        }

        public int getStartOffset() {
            return leaf.getStartOffset();
        }

        public Element getParentElement() {
            return leaf.getParentElement();
        }

        public String getName() {
            return tag.toString();
        }

        public int getEndOffset() {
            return leaf.getEndOffset();
        }

        public int getElementIndex(int offset) {
            return leaf.getElementIndex(offset);
        }

        public int getElementCount() {
            return leaf.getElementCount();
        }

        public Element getElement(int index) {
            return leaf.getElement(index);
        }

        public Document getDocument() {
            return leaf.getDocument();
        }

        public AttributeSet getAttributes() {
            return attributes;
        }
    }

    /**
     * Is this element equivalent to a hyperlink tag in HTML?
     *
     * A hyperlink is an inline element. There are two specifics for that in Swing:
     *
     * 1. Inline elements will be modeled in general as CONTENT elements with specific attributes,
     * their attribute set contains one attribute of type HTML.Tag, the value of which is
     * an attribute set itself, i.e. the attributes of the inline element.
     *
     * Such CONTENT element have no equivalent in HTML. If we want to compare such Swing elements
     * to HTML, we must make them invisible and emulate their HTML counterpart.
     *
     * 2. Some elements like TD require an additional artificial element called IMPLIED
     * for any CONTENT as an intermediate between the TD and the CONTENT.
     *
     * It behaves like a P tag, but has no counterpart in HTML. Again, this must be
     * made invisible when comparing it to HTML.
     *
     * If both conditions are combined, i.e. we have a TD with hyperlink (A) content,
     * there are two artificial elements present IMPLIED and below it CONTENT.
     * 
     * @param e
     * @return
     */
    public boolean isHyperlink(Element e) {
        String name = e.getName();
        if (isArtificialElement(e)) {
            if (e.getElementCount() > 0)
                if (e.getElement(0).getAttributes().getAttribute(HTML.Tag.A) != null)
                    return true;
        } else if (isInlineElement(e))
            if (e.getAttributes().getAttribute(HTML.Tag.A) != null)
                return true;

        return false;
    }

    public boolean isDataHyperlink(Element e) {
        String name = e.getName();
        if (name.equals(HTML.Tag.IMPLIED.toString())) {
            if (e.getElementCount() > 0)
                if (e.getElement(0).getAttributes().getAttribute(HTML.Tag.A) != null
                        && ((AttributeSet) e.getElement(0).getAttributes().getAttribute(HTML.Tag.A)).isDefined(ATTRIBUTE_NAME_DATA))
                    return true;
        } else if (isInlineElement(e))
            if (e.getAttributes().getAttribute(HTML.Tag.A) != null
                && ((AttributeSet) e.getAttributes().getAttribute(HTML.Tag.A)).isDefined(ATTRIBUTE_NAME_DATA))
                return true;


        return false;
    }

    public Element getRealParagraph(int pos) {
        Element e = getDocument().getParagraphElement(pos);
        if (isDataHyperlink(e))
            return (e);

        return super.getRealParagraph(pos);
    }

    public boolean isLeaf(Element e) {
        if (e.isLeaf())
            return true;

        if (e instanceof StructureTree.XMLElement)
            return false;

        for (int i = 0; i < e.getElementCount(); i++) {
            if (!isContent(e.getElement(i)))
                return false;
        }

        return true;
    }

    public String exportData(
            Element styleSheetElement) throws Exception {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        DataWriter writer = new DataWriter(this);
        writer.write(styleSheetElement, out);
        return out.toString();
    }

    public String exportData() throws Exception {
        Element styleSheetElement = findTemplate(getDocument().getRootElements()[0]);
        return exportData(styleSheetElement);
    }

    public void addDataMappings(Element e, Collection mappings) throws Exception {
        if (isTemplate(e)) {
            String data = exportData(e);
            String path = getPath(e);
            String type = getSchemaName(e);

            UploadData.Request upload = new UploadData.Request();
            upload.setPath(path);
            upload.setData(data);
            upload.setType(type);

            mappings.add(upload);
            return;
        }

        for (int i = 0; i <
                e.getElementCount(); i++) {
            addDataMappings(e.getElement(i), mappings);
        }

    }

    public Collection getDataMappings() throws Exception {
        ArrayList mappings = new ArrayList();
        Element root = getDocument().getRootElements()[0];

        addDataMappings(root, mappings);

        return mappings;
    }

    public int getMaximumSecondaryQueryIndex() {
        Element root = getDocument().getRootElements()[0];
        return getMaximumSecondaryQueryIndex(root, 0);
    }

    protected int getMaximumSecondaryQueryIndex(Element e, int max) {
        int index = getSecondaryQueryIndex(e);
        if (index > max)
            max = index;

        for (int i = 0; i < e.getElementCount(); i++) {
            max = getMaximumSecondaryQueryIndex(e.getElement(i), max);
        }

        return max;
    }

    public boolean isSecondaryQuery(Element e) {
        AttributeSet atts;
        if (isArtificialWithInlineElement(e))
            atts = getFirstInlineAttributes(e);
        else
            atts = e.getAttributes();

        if (!atts.isDefined(ATTRIBUTE_NAME_DATA))
            return false;
        String data = (String) atts.getAttribute(ATTRIBUTE_NAME_DATA);
        if (!data.startsWith(SECONDARY_QUERY))
            return false;

        return true;
    }

    public boolean isSecondaryQueryRoot(Element e) {
        AttributeSet atts;
        if (isArtificialWithInlineElement(e))
            atts = getFirstInlineAttributes(e);
        else
            atts = e.getAttributes();

        if (!atts.isDefined(ATTRIBUTE_NAME_DATA))
            return false;
        String data = (String) atts.getAttribute(ATTRIBUTE_NAME_DATA);
        if (!data.startsWith(SECONDARY_QUERY))
            return false;

        return atts.isDefined(ATTRIBUTE_NAME_SCHEMA);
    }

    public String getSecondaryQueryKey(
            Element e) {
        if (!isSecondaryQuery(e))
            return null;

        AttributeSet atts;

        if (isArtificialWithInlineElement(e))
            atts = getFirstInlineAttributes(e);
        else
            atts = e.getAttributes();

        String data = (String) atts.getAttribute(ATTRIBUTE_NAME_DATA);
        if (!data.startsWith(SECONDARY_QUERY))
            return null;
        return data.substring(0, data.indexOf("/", SECONDARY_QUERY.length()));
    }

    public String getRootData(
            Element e, String nodeTypeName) {
        String data = (String) e.getAttributes().getAttribute(ATTRIBUTE_NAME_DATA);
        if (data.endsWith(nodeTypeName))
            return data;

        return getRootData(e.getParentElement(), nodeTypeName);
    }

    public String getSecondaryQueryValue(
            Element e) {
        if (!isSecondaryQuery(e))
            return null;

        AttributeSet atts;

        if (isArtificialWithInlineElement(e))
            atts = getFirstInlineAttributes(e);
        else
            atts = e.getAttributes();

        String data = (String) atts.getAttribute(ATTRIBUTE_NAME_DATA);
        if (!data.startsWith(SECONDARY_QUERY))
            return null;
        return data.substring(data.indexOf("/", SECONDARY_QUERY.length()) + 1);
    }

    protected int getSecondaryQueryIndex(Element e) {
        String secondaryQueryKey = getSecondaryQueryKey(e);
        if (secondaryQueryKey == null)
            return 0;

        int index = 0;
        try {
            index = Integer.parseInt(secondaryQueryKey.substring(SECONDARY_QUERY.length()));
        } catch (NumberFormatException ex) {
            ex.printStackTrace();
        }

        return index;
    }

    protected void removeSecondaryQueries(Element e, String key) {
        if (e.getAttributes().isDefined(ATTRIBUTE_NAME_DATA) && ((String) e.getAttributes().getAttribute(ATTRIBUTE_NAME_DATA)).startsWith(key))
            removeAttribute(e, ATTRIBUTE_NAME_DATA);

        for (int i = 0; i <
                e.getElementCount(); i++) {
            removeSecondaryQueries(e.getElement(i), key);
        }

    }

    public void removeSecondaryQueries(Element e) {
        String key = getSecondaryQueryKey(e);
        if (key == null)
            return;

        removeSecondaryQueries(e, key);
    }

    /**
     * Create a map of secondary queries from the HTML code.
     * 
     * @return the secondary query map
     */
    public HashMap<String,String> getSecondaryQueriesFromHtml() {
        HashMap<String,String> result = new HashMap<String,String>();

        Element root = getContext().getDocument().getRootElements()[0];
        addSecondaryQueries(root, result);

        return result;
    }

    protected void addSecondaryQueries(Element e, HashMap<String,String> secondaryQueries) {
        if (isSecondaryQueryRoot(e)) {
            secondaryQueries.put(getSecondaryQueryKey(e).substring(1), getSecondaryQueryValue(e));
        }

        for (int i = 0; i < e.getElementCount(); i++)
            addSecondaryQueries(e.getElement(i), secondaryQueries);
    }

    public void mapSchemaElement(Element e, String schema) throws BadLocationException, IOException {
        if (schema == null || schema.length() == 0)
            removeAttribute(e, ATTRIBUTE_NAME_SCHEMA);
        else
            addAttribute(e, ATTRIBUTE_NAME_SCHEMA, schema);
    }

    public void mapDataElement(Element e, String data) throws BadLocationException, IOException {
        mapDataElement(e, data, null);
    }

    public void mapDataElement(Element e, String data, String view) throws BadLocationException, IOException {
        if (data == null || data.length() == 0)
            removeAttribute(e, ATTRIBUTE_NAME_DATA);
        else
            addAttribute(e, ATTRIBUTE_NAME_DATA, data);
        
        if (view == null || view.length() == 0)
            removeAttribute(e, ATTRIBUTE_NAME_VIEW);
        else
            addAttribute(e, ATTRIBUTE_NAME_VIEW, view);
    }

    public void mapDataElement(int start, int end, String data) throws BadLocationException, IOException {
        Element e = getDocument().getParagraphElement(start);
        if (e != getDocument().getParagraphElement(end))
            return;

        SimpleAttributeSet att = new SimpleAttributeSet();
        att.addAttribute(ATTRIBUTE_NAME_DATA, data);

        StringBuffer buf = new StringBuffer();
        int length = start - e.getStartOffset();
        if (length > 0)
            buf.append(getDocument().getText(e.getStartOffset(), length));
        buf.append("<span " + ATTRIBUTE_NAME_DATA + "=\"" + data + "\"" + ">");
        length =
                end - start;
        if (length > 0)
            buf.append(getDocument().getText(start, length));
        buf.append("</span>");
        length =
                e.getEndOffset() - end;
        if (length > 0)
            buf.append(getDocument().getText(end, length));

        getDocument().setInnerHTML(e, buf.toString());
    }

    public Element insertDataBlock(
            Element start, Element end, File schema, String schemaRoot, File template) throws BadLocationException, IOException {
        if (start.getParentElement() != end.getParentElement())
            return null;

        int pos = start.getStartOffset();
        int startIndex = getChildIndex(start);
        int endIndex = getChildIndex(end);
        Element parent = start.getParentElement();

        StringBuffer buf = new StringBuffer();
        buf.append("<" + HTML.Tag.DIV);
        if (schema != null)
            buf.append(" " + ATTRIBUTE_NAME_SCHEMA + "=\"" + schema.getPath() + "\"");
        buf.append(">");

        for (int i = startIndex; i <=
                endIndex; i++) {
            buf.append(getOuterHTML(parent.getElement(i)));
        }

        buf.append("</" + HTML.Tag.DIV + ">");

        if (startIndex < endIndex) {
            Element e = null;
            do {
                e = parent.getElement(startIndex + 1);
                remove(e);
            } while (e != end);
        }

        getDocument().setOuterHTML(start, buf.toString());

        Element templateElement = getAncestor(pos, HTML.Tag.DIV);
        addAttribute(templateElement, ATTRIBUTE_NAME_TEMPLATE, template.getPath());
        addAttribute(templateElement, ATTRIBUTE_NAME_DATA, schemaRoot);

        BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(template));
        saveTemplate(templateElement, out);
        out.close();

        return templateElement;
    }

    public void removeDataElement(Element e) throws BadLocationException, IOException {
        String secondaryQueryKey = null;
        if (isSecondaryQuery(e)) {
            String value = getSecondaryQueryValue(e);
            if (value.indexOf("/") < 0) { // secondary query root
                String key = getSecondaryQueryKey(e);
                HashMap<String, String> queries = (HashMap<String, String>) getProperties().get(Editor.PROP_SECONDARY_QUERIES);
                queries.remove(key.substring(1));
                removeSecondaryQueries(e);

                return;
            }

        }
        removeAttribute(e, ATTRIBUTE_NAME_DATA);
    }

    public void removeDataBlock(Element template) throws BadLocationException, IOException {
        String innerHtml = getInnerHTML(template);
        getDocument().setOuterHTML(template, innerHtml);
    }

    protected String getParentName(String name) {
        if (name.indexOf("/") < 0)
            return name;
        return name.substring(0, name.lastIndexOf("/"));
    }

    public void insertDataElement(String name, Element parent) throws Exception {
        if (parent == null)
            return;

        String styleSheet = (String) parent.getAttributes().getAttribute(ATTRIBUTE_NAME_TEMPLATE);
        String schema = (String) parent.getAttributes().getAttribute(ATTRIBUTE_NAME_SCHEMA);

        Template template = getTemplate(styleSheet, getParentName(name));
        if (template == null)
            return;

        insertDataElement(name, parent, (VirtualElement) template.getRoot());
    }

    protected String discardFilters(String name) {
        return name.replaceAll("\\[.*\\]", "");
    }

    public String getElementName(
            Element e) {
        if (isData(e))
            return discardFilters(getData(e));
        return e.getName();
    }

    public String getDataName(
            VirtualElement e) {
        return getElementName(e);
    }

    public boolean occursMultiple(Element element) throws Exception {
        String templateName = getTemplateName(element);
        if (templateName == null)
            return false;

        VirtualBlock virtualBlock = VirtualBlock.getVirtualBlock(templateName, this);
        if (virtualBlock != null) {
            Template template = virtualBlock.getTemplateByElementPath(discardFilters(getData(element)));
            if (template != null && template.occursMultiple())
                return true;
        }

        return false;
    }

    public boolean occursMultipleVirtual(VirtualElement v) throws Exception {
        VirtualBlock virtualBlock = v.getVirtualBlock();
        if (virtualBlock != null) {
            if (!isData(v))
                return false;
            Template template = virtualBlock.getTemplateByElementPath(discardFilters(getData(v)));
            if (template != null && template.occursMultiple())
                return true;
        }

        return false;
    }

    protected boolean insertDataElement(String name, Element element, VirtualElement data) throws Exception {
        boolean found = false;
        StringBuffer buf = new StringBuffer();

        Element child = null;
        int i = 0, j = 0;
        while (i < data.getElementCount()) {
            VirtualElement childData = (VirtualElement) data.getElement(i);
            if (element.getElementCount() > j)
                child = element.getElement(j);
            else
                child = null;
            if (child != null && getElementName(child).equals(getDataName(childData))) {
                if (insertDataElement(name, child, childData))
                    return true;
                buf.append(getOuterHTML(child));
                j++;

                if (occursMultiple(childData))
                    continue;
            } else if (childData != null && childData.isDataElement()) {
                Template template = childData.getVirtualBlock().getTemplateByName(childData.getName());
                if (template != null && template.getElementPath().equals(name)) {
                    buf.append(template.getHtml());
                    found =
                            true;
                }

            }

            i++;
        }

        if (found)
            getDocument().setInnerHTML(element, buf.toString());

        return found;
    }

    public Element nextDataElement(
            int pos) throws Exception {
        String name = getTemplateName(getDocument().getParagraphElement(pos));
        VirtualBlock virtualBlock = VirtualBlock.getVirtualBlock(name, this);
        return virtualBlock.nextDataElement(pos);
    }

    public Element previousDataElement(
            int pos) throws Exception {
        String name = getTemplateName(getDocument().getParagraphElement(pos));
        VirtualBlock virtualBlock = VirtualBlock.getVirtualBlock(name, this);
        return virtualBlock.previousDataElement(pos);
    }

    public static boolean isSecondaryQueryRoot(String name) {
        if (!name.startsWith(SECONDARY_QUERY))
            return false;
        String[] parts = name.split("/");
        if (parts.length == 3) // like /q1/xxx
            return true;
        return false;
    }

    /**
     * Set the n-edit attribute to false
     * @param pos
     */
    public void markReadOnly(Element e) {
        SimpleAttributeSet atts = new SimpleAttributeSet(e.getAttributes());
        atts.addAttribute(ATTRIBUTE_NAME_EDIT, ATTRIBUTE_VALUE_FALSE);

        getDocument().setElementAttributes(e, atts, true);
    }

    /**
     * Set the n-binary attribute to true
     * @param pos
     */
    public void markBinary(Element e) {
        SimpleAttributeSet atts = new SimpleAttributeSet(e.getAttributes());
        atts.addAttribute(ATTRIBUTE_NAME_BINARY, ATTRIBUTE_VALUE_TRUE);

        getDocument().setElementAttributes(e, atts, true);
    }

    /**
     * If the n-edit attribut is present and false, remove it.
     * @param pos
     */
    public void markReadWrite(Element e) {
        if (!e.getAttributes().isDefined(ATTRIBUTE_NAME_EDIT))
            return;

        SimpleAttributeSet atts = new SimpleAttributeSet(e.getAttributes());
        atts.removeAttribute(ATTRIBUTE_NAME_EDIT);
        if (atts.containsAttribute(ATTRIBUTE_NAME_BINARY, ATTRIBUTE_VALUE_TRUE))
            atts.removeAttribute(ATTRIBUTE_NAME_BINARY);

        getDocument().setElementAttributes(e, atts, true);
    }

    /**
     * Is the n-edit attribut present and has it the value false?
     * 
     * @param pos
     * @return
     */
    public boolean isReadOnly(Element e) {
        if (!e.getAttributes().isDefined(ATTRIBUTE_NAME_EDIT))
            return false;

        if (e.getAttributes().getAttribute(ATTRIBUTE_NAME_EDIT).equals(ATTRIBUTE_VALUE_FALSE))
            return true;

        return false;
    }

    /**
     * Check, if the current element can be edited. It cannot be edited, if this has either been explicitly denied (n-edit="false") or
     * if there is no specific attribute, but a parent node requires its children to explicitly allow editing (n-edit="explicit") 
     * and the current element has not.
     * 
     * @param e The current element
     * @return true, if the current element shall be edited by the user, else false.
     */
    public boolean canEdit(Element e) {
        AttributeSet atts = null;
        if (isInlineDataElement(e))
            atts = getInlineAttributes(e);
        else
            atts = e.getAttributes();
        
        if (atts.isDefined(DataModel.ATTRIBUTE_NAME_EDIT)) {
            String editValue = (String) atts.getAttribute(DataModel.ATTRIBUTE_NAME_EDIT);
            if (editValue.equals(DataModel.ATTRIBUTE_VALUE_TRUE))
                return true;
            else if (editValue.equals(DataModel.ATTRIBUTE_VALUE_FALSE))
                return false;
        }

        Element parent = e.getParentElement();
        while (parent != null) {
            if (parent.getAttributes().isDefined(DataModel.ATTRIBUTE_NAME_EDIT) ) {
                String edit = (String) parent.getAttributes().getAttribute(ATTRIBUTE_NAME_EDIT);
                if (edit.equals(ATTRIBUTE_VALUE_TRUE))
                    return true;
                else
                    return false;
            }
            parent = parent.getParentElement();
        }

        return true;
    }

    public boolean canEdit(int pos) {
        return canEdit(getDocument().getCharacterElement(pos));
    }

    public boolean readOnly(Element e) {
        if (e.getAttributes().isDefined(DataModel.ATTRIBUTE_NAME_EDIT))
            if (e.getAttributes().getAttribute(DataModel.ATTRIBUTE_NAME_EDIT).equals(DataModel.ATTRIBUTE_VALUE_FALSE))
                return true;

        return false;
    }

    /**
     * Return the parent node type
     *
     * @param e The element, whose parent node type shall be determined
     * @return the parent node type.
     */
    public String getParentNodeType(Element e) {
        String result = null;
        while (result == null && e != null) {
            String data = (String) e.getAttributes().getAttribute(DataModel.ATTRIBUTE_NAME_DATA);

            if (data != null && data.indexOf("@") >= 0) { // this is a property mapping. We want the node type
                e = e.getParentElement();
                continue;
            }

            result = data;
        }

        return result;
    }

    /**
     * Is this a table, that corresponds to references from the primary to the secondary?
     * Such a table will have the ATTRIBUTE_NAME_PRIMARY_REFERENCE in its TR elements.
     *
     * @param pos
     * @return
     */
    public boolean isFromThisTable(int pos) {
        Element e = getDocument().getParagraphElement(pos);

        while (e != null && !(e.getName().equals(HTML.Tag.TR.toString()) && e.getAttributes().isDefined(DataModel.ATTRIBUTE_NAME_PRIMARY_REFERENCE)))
            e = e.getParentElement();

        return e != null;
    }


    /**
     * Return the external template attribute
     * 
     * @param e
     * @return
     */
    public String getExistingTemplateUuid(Element e) {
        return (String) e.getAttributes().getAttribute(ATTRIBUTE_NAME_EXTERNAL_TEMPLATE);
    }

    /**
     * Substitute the element with the template, enclose it in a DIV.
     * Set the n-data and n-external-template attributes for the div.
     * 
     * @param e
     * @param nodeType
     * @param templateUuid
     * @param templateHtml
     * @throws BadLocationException
     * @throws IOException
     */
    public void setExistingTemplate(Element e, String nodeType, String templateUuid, String templateHtml) throws BadLocationException, IOException {
        String data = getData(e);
        if (data == null)
            return;

        String schema = "";
        if (e.getAttributes().isDefined(ATTRIBUTE_NAME_SCHEMA)) { // this was the primary node type.
            // add schema again
            schema = ATTRIBUTE_NAME_SCHEMA + "=\"" + e.getAttributes().getAttribute(ATTRIBUTE_NAME_SCHEMA) + "\" ";
            // remove all current secondary queries
            HashMap<String,String> secondaryQueries = (HashMap<String,String>) properties.get(Editor.PROP_SECONDARY_QUERIES);
            if (secondaryQueries != null)
                secondaryQueries.clear();
        }

        int start = templateHtml.indexOf("<" + HTML.Tag.DIV.toString());
        if (start < 0)
            return;

        start = templateHtml.indexOf(">", start);
        start++;

        int end = templateHtml.lastIndexOf("</" + HTML.Tag.DIV.toString());
        templateHtml = templateHtml.substring(start, end);

        templateHtml = templateHtml.replaceAll("\"" + nodeType, "\"" + data);
        SecondaryQueryAdjuster adjuster = new SecondaryQueryAdjuster(templateHtml, (HashMap<String,String>) properties.get(Editor.PROP_SECONDARY_QUERIES));
        adjuster.adjust();
        templateHtml = adjuster.getHtml();

        setOuterHTML(e, "<div " + schema + ATTRIBUTE_NAME_DATA + "=\"" + data + "\" " + ATTRIBUTE_NAME_EXTERNAL_TEMPLATE + "=\"" + templateUuid + "\">" + templateHtml + "</div");
    }
    
    /**
     * Return the model of the browser element actions.
     * 
     * @return the model
     */
    public BrowserElementActionModel getBrowserElementActionModel() {
        return new BrowserElementActionModel(this);
    }

    /**
     * Return the name of a named query
     * @param e
     * @return
     */
    public String getQueryName(Element e) {
        if (!e.getAttributes().isDefined(ATTRIBUTE_NAME_SELECT))
            return null;
        return (String) e.getAttributes().getAttribute(ATTRIBUTE_NAME_SELECT);
    }

    /**
     * Add the named query attribute to the element.
     * If the queryName is null, remove the attribute.
     * 
     * @param e
     * @param queryName
     */
    public void setQueryName(Element e, String queryName) {
        if (queryName == null || queryName.length() == 0)
            removeAttribute(e, ATTRIBUTE_NAME_SELECT);
        else
            addAttribute(e, ATTRIBUTE_NAME_SELECT, queryName);
    }

    /**
     * Return the values of the n-select attributes in the
     * document.
     *
     * @return    public String getQueryName(Element e) {
        if (!e.getAttributes().isDefined(ATTRIBUTE_NAME_SELECT))
            return null;
        return (String) e.getAttributes().getAttribute(ATTRIBUTE_NAME_SELECT);
    }

     */
    public String[] saveSelectQueries() {
        List<String> queries = new LinkedList<String>();
        Element root = getContext().getDocument().getRootElements()[0];
        addSelectQueries(root, queries);

        return queries.toArray(new String[queries.size()]);
    }

    /**
     * Add the values of the query attribute associated
     * to the element and its children
     * 
     * @param e
     * @param queries
     */
    public void addSelectQueries(Element e, List<String> queries) {
        String query = getQueryName(e);
        if (query != null)
            queries.add(query);

        for (int i = 0; i < e.getElementCount(); i++)
            addSelectQueries(e.getElement(i), queries);
    }

    /**
     * Return the name of an action associated to the element.
     * 
     * @param e the element
     * @return the action
     */
    public String getElementAction(Element e) {
        if (!e.getAttributes().isDefined(ATTRIBUTE_NAME_ACTION))
            return null;
        return (String) e.getAttributes().getAttribute(ATTRIBUTE_NAME_ACTION);
    }

    /**
     * Add the action to the element.
     * 
     * If the action name is null, remove the attribute.
     * 
     * @param e
     * @param queryName
     */
    public void setElementAction(Element e, String queryName) {
        if (queryName == null || queryName.length() == 0)
            removeAttribute(e, ATTRIBUTE_NAME_ACTION);
        else
            addAttribute(e, ATTRIBUTE_NAME_ACTION, queryName);
    }

    /**
     * Return the element actions of the document.
     * 
     * @return the element actions
     */
    public String[] getElementActions() {
        List<String> elementActions = new LinkedList<String>();
        Element root = getContext().getDocument().getRootElements()[0];
        addElementActions(root, elementActions);

        return elementActions.toArray(new String[elementActions.size()]);
    }

    /**
     * Add the values of the query attribute associated
     * to the element and its children
     * 
     * @param e
     * @param queries
     */
    public void addElementActions(Element e, List<String> elementActions) {
        String query = getQueryName(e);
        if (query != null)
            elementActions.add(query);

        for (int i = 0; i < e.getElementCount(); i++)
            addElementActions(e.getElement(i), elementActions);
    }

    /**
     * Mark an element as reference to a page
     *
     * @param e the element
     * @param mark if true mark the element else remove the mark
     */
    public void markAsPageReference(Element e, boolean mark) {
        if (mark)
            addAttribute(e, ATTRIBUTE_NAME_PAGE, ATTRIBUTE_VALUE_TRUE);
        else
            removeAttribute(e, ATTRIBUTE_NAME_PAGE);
    }

    /**
     * Mark an element as reference to a component
     *
     * @param e the element
     * @param mark if true mark the element else remove the mark
     */
    public void markAsComponentReference(Element e, boolean mark) {
        if (mark)
            addAttribute(e, ComponentModel.ATT_COMPONENT_ID, ATTRIBUTE_VALUE_COMPONENT_ID);
        else
            removeAttribute(e, ComponentModel.ATT_COMPONENT_ID);
    }

    /**
     * Does the element represent a property?
     *
     * @param e the element
     * @return
     */
    public boolean isProperty(Element e) {
        String data = getData(e);
        return data.indexOf("@") >= 0;
    }
    
    /**
     * For a component element, return the actual start tag
     * 
     * @param e1 the virtual element
     * @param e2 the actual element
     * @return 
     */
    public String getStartTag(Element e1, Element e2) {
        if (!e1.getAttributes().isDefined(ComponentModel.ATT_COMPONENT_ID))
            return getStartTag(e1.getAttributes());

        return getStartTag(substituteAttribute(
                e1.getAttributes(),
                ComponentModel.ATT_COMPONENT_ID,
                e2.getAttributes().getAttribute(ComponentModel.ATT_COMPONENT_ID)));
    }

    /**
     * For a component element, highlight the actual start tag.
     * 
     * @param e1 virtual element
     * @param e2 actual element
     * @param additionalAttribute
     * @return 
     */
    public String getStartTag(Element e1, Element e2, String additionalAttribute) {
        if (!e1.getAttributes().isDefined(ComponentModel.ATT_COMPONENT_ID))
            return getStartTag(e1.getAttributes(), additionalAttribute);
        
        return getStartTag(substituteAttribute(
                e1.getAttributes(), 
                ComponentModel.ATT_COMPONENT_ID,
                e2.getAttributes().getAttribute(ComponentModel.ATT_COMPONENT_ID)),
                additionalAttribute);
    }

    
}
